/**
 * Copyright 2009 Jesse Brown (jbrown@fluentv.com)
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 *   http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */
package com.fluentv.games.netblocks;

import java.util.Timer;
import java.util.TimerTask;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.graphics.Color;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.fluentv.games.netblocks.PlayingField.MoveDirection;
import com.fluentv.games.netblocks.piece.Shape;

/**
 * Overall View to wrap up gameplay.
 * 
 * @author Jesse Brown (jbrown@fluentv.com)
 */
public class PlayingView extends LinearLayout {
  
  
  private static final String LOG_TAG = "PlayingView";
  
  // Lookup keys when serializing/deserializing
  private static final int formatVersion = 3;
  private static final String FORMAT_KEY = "SerializedFormat";
  private static final String INITIALIZED_KEY = "Initialized";
  private static final String CURRENT_MOVEMENT_DELAY_KEY = "CurrentMovementDelay";
  private static final String SOLVED_ROWS_KEY = "SolvedRows";
  private static final String CURRENT_SCORE_KEY = "CurrentScore";
  private static final String CURRENT_LEVEL_KEY = "CurrentLevel";
  private static final String CURRENTLY_RUNNING_KEY = "CurrentlyRunning";
  
  //Challenge - make it configuration?
  private static final int rowsPerLevel = 5;
  private static final int initialMovementDelay = 1000;
  private static final float perLevelMovementMultiplier = .8F;

  
  //'Move' timer to tick us forward when the time comes (doesn't get serialized)
  private transient Timer moveTimer; 

  private long currentMovementDelay;  
  private int solvedRows;
  private int currentScore;    
  private int currentLevel;  
  private boolean initialized;
    
  private PlayingField playingField;
  private PreviewView preview;
  
  private transient SoundEffectsPlayer soundEffects;
  private transient GestureDetector gestureDetector;
  
  public PlayingView(Context context, final SoundEffectsPlayer soundEffects, int fieldSize) {
    super(context);    
    this.soundEffects = soundEffects;
    
    // Make sure to clear out any of the old views (if they exist).
    removeAllViews();    
               
    setOrientation(VERTICAL);
    inflate(context, R.layout.scoreboard_horizontal, this);

    preview = new PreviewView(context, 4, 2);
    
    LayoutParams previewParams = 
      new LayoutParams(LayoutParams.WRAP_CONTENT, 50);
    previewParams.gravity = Gravity.CENTER;          
    addView(preview, previewParams);
    
    LayoutParams playingFieldParams = new LayoutParams(
        LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
    playingFieldParams.gravity = Gravity.CENTER;          
    
    playingField = new PlayingField(context, fieldSize, fieldSize);    
    addView(playingField, playingFieldParams);
    
    inflate(context, R.layout.playingview_controls, this);
   
    final Button dropButton = (Button) findViewById(R.id.button_drop);
    dropButton.setBackgroundColor(Color.TRANSPARENT);
    dropButton.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {             

        // Drop the block on the field.
        playingField.dropCurrentShape();
      }
    }); 
  }
 
  /**
   * Add our gesture listener referring to the specific playing field.
   */
  
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    if (gestureDetector == null) {
      gestureDetector = new GestureDetector(
        new GestureMovementListener(soundEffects, playingField));
    }
    
    return gestureDetector.onTouchEvent(event);    
  }
  
  /**
   * Reset the game, resetting any previous values and resetting our 'move' timer.
   */
  public synchronized void resetGame() {
    playingField.reset();    
    
    currentScore = 0;
    currentLevel = 1;
    solvedRows = 0;    
    currentMovementDelay = initialMovementDelay;
    
    refreshDashboard();
    
    endPieceMovement();
    initialized = true;
  }
  
  /**
   * Start piece movement using a background timer thread. If pieces are already moving, does nothing.
   */
  public synchronized void startPieceMovement() {
    if (!initialized) {
      Log.w("PlayingField", "View has not yet been initialized, cannot start piece movement.");
      return;
    }
    
    if (moveTimer != null) {
      return;
    }    
        
    Log.v("PlayingField", "Started piece movement with current delay " + currentMovementDelay);
    moveTimer = new Timer("Playing field movement timer");
    moveTimer.scheduleAtFixedRate(
        new MovePieceTimerTask(), 0, currentMovementDelay); 
    
    soundEffects.startLevel();
  }   
  
  /**
   * End piece movement (cleans up background threads)
   */
  public synchronized void endPieceMovement() {
    if (moveTimer != null) {
      Log.v("PlayingField", "Stopped piece movement");
      moveTimer.cancel();
      moveTimer = null;
    }   
  }
  
  /**
   * Attempt to rotate a new piece onto the playing field. Takes the current preview
   * shape, and try to put it onto the field (generating a replacement for it.
   */
  private boolean rotatePreviewShapeToField() {
    synchronized (playingField) {
      synchronized (preview) {
        playingField.freezeCurrentShape();
        newlySolvedRows(playingField.findSolvedRows());
        
        Shape previewedShape = preview.getShape();
        if (previewedShape == null) {
          previewedShape = ShapeFactory.getInstance().buildRandomShape();
        }
        
        Shape nextShape = ShapeFactory.getInstance().buildRandomShape();       
        
        if (playingField.addShape(previewedShape)) {
          preview.setShape(nextShape);
          return true;
        } else {
          return false;
        }
      }
    }
  }
  
  /**
   * TimerTask to move the current piece down the field at regular intervals, adding a new random shape when movement is no longer possible. 
   */
  private class MovePieceTimerTask extends TimerTask {
    @Override
    public void run() {      
      synchronized (playingField) {
        if (!playingField.moveCurrentShape(MoveDirection.DOWN)) {

          Log.i("MovePiece", 
              "Could not move piece (it clipped), adding new shape and attempting to find all solved rows.");
          if (rotatePreviewShapeToField()) {
            soundEffects.newBlock();                 
          } else {
            // TODO - pop up a dialog? Lost!
            endPieceMovement();
            soundEffects.lostGame();
          }
        } else {
          Log.d("MovePiece", "Successfully moved piece down");
          soundEffects.timedMoveBlock();
        }
      }
    }    
  }
   
  /**
   * Add to the score based on the number of newly solved rows..
   */
  private synchronized void newlySolvedRows(int newRows) {
    if (newRows > 0) {
      solvedRows += newRows;
      soundEffects.solvedRow();
      
      Log.i("NewlySolvedRows", "Solved " + solvedRows);
            
      // Do level changing if necessary.
      int newLevel = (solvedRows / rowsPerLevel) + 1;
      if (currentLevel < newLevel) {
        currentLevel = newLevel;
                
        currentMovementDelay = (long) (currentMovementDelay * perLevelMovementMultiplier);
        endPieceMovement();
        startPieceMovement();        
      }
      
      currentScore += (newRows * 1000 * currentLevel);                  
      refreshDashboard();
    }
  }
 
  /**
   * Update scores, levels, and other dashboard stuff.
   */
  private synchronized void refreshDashboard() {
    updateTextView(R.id.score_value, currentScore);
    updateTextView(R.id.level_value, currentLevel);
    updateTextView(R.id.rows_solved_value, solvedRows);      
  }
  
  /**
   * Update a TextView value if the value has changed (we MUST update in the UI thread 
   * or our changes get lost)
   */
  private void updateTextView(int id, final int value) {
    final TextView textView = (TextView)findViewById(id);
    textView.post(new Runnable() {
      @Override
      public void run() {        
        textView.setText(String.valueOf(value));
      }    
    });         
  }
  
  /**
   * Clean up any resources (background threads, network connections, etc.) in use by the field.
   */
  public void destroy() {
    endPieceMovement();
  }
  
  /**
   * @return true if a valid PlayingView can be restored. 
   */
  public boolean hasRestorableState(SharedPreferences sharedPreferences) {
    if (sharedPreferences == null || 
        !sharedPreferences.getBoolean(INITIALIZED_KEY, false)) {
      
      Log.w(LOG_TAG, "No state, or state not initialized.");
      return false;
    }
    
    if (sharedPreferences.getInt(FORMAT_KEY, 0) != formatVersion) {
      Log.v(LOG_TAG, "Persisted data format is not our current version, ignoring.");
      return false;
    }

    return true;
  }
  
  /**
   * Called when the Activity needs to restore the view ot a certain state.
   * @param sharedPreferences Previously serialized data (of available)
   */
  public synchronized void restoreState(SharedPreferences sharedPreferences) {
    
    // Make sure to nuke our transient data (undoubtably not correct anymore, even if it did get persisted)
    resetGame();
    
    if (!hasRestorableState(sharedPreferences)) {
      return;
    }
    
    // Restore our private fields.
    currentMovementDelay = sharedPreferences.getLong(CURRENT_MOVEMENT_DELAY_KEY, 0);
    solvedRows = sharedPreferences.getInt(SOLVED_ROWS_KEY, 0);
    currentScore = sharedPreferences.getInt(CURRENT_SCORE_KEY, 0);
    currentLevel = sharedPreferences.getInt(CURRENT_LEVEL_KEY, 0);   
    
    // Restore the field.
    playingField.restoreState(sharedPreferences);
    preview.restoreState(sharedPreferences);
    
    Log.i(LOG_TAG, "Loaded score and level information (movement delay " 
        + currentMovementDelay + ", solved Rows " + solvedRows + ", current score " 
        + currentScore + ", current level " + currentLevel);
    
    refreshDashboard();  
    
    if (sharedPreferences.getBoolean(CURRENTLY_RUNNING_KEY, false)) {
      startPieceMovement();
    }
  }

  /**
   * Called then the Activied needs to persist the current state.
   * @param editor Bundle to persist the state to.
   */
  public synchronized void saveState(Editor editor) {

    if (!initialized) {
      Log.v(LOG_TAG, "No state to save (not yet initialized)");
      return;
    }
    
    editor.putLong(CURRENT_MOVEMENT_DELAY_KEY, currentMovementDelay);
    editor.putInt(SOLVED_ROWS_KEY, solvedRows);
    editor.putInt(CURRENT_SCORE_KEY, currentScore);
    editor.putInt(CURRENT_LEVEL_KEY, currentLevel);
    
    // Flush out the field.
    playingField.saveState(editor);
    preview.saveState(editor);
    
    if (moveTimer != null) {
      editor.putBoolean(CURRENTLY_RUNNING_KEY, true);
    }
    
    editor.putBoolean(INITIALIZED_KEY, true);
    editor.putInt(FORMAT_KEY, formatVersion);
    
    Log.i(LOG_TAG, "Persisted current score and level information");  
  }    
}
